Home:ALL Converter>Why must C++ function parameter packs be placeholders or pack expansions?

Why must C++ function parameter packs be placeholders or pack expansions?

Ask Time:2022-05-29T11:32:07         Author:user3188445

Json Formatter

The declarator for a C++20 function parameter pack must either be a placeholder or a pack expansion. For example:

// OK, template parameter pack only, no function parameter pack
template<unsigned ...I> void good1() {}

// OK function parameter pack is pack expansion of decltype(I)
template<unsigned ...I> void good2(decltype(I)...i) {}

// OK contains placeholder auto
void good3(std::same_as<unsigned> auto...i) {}

// OK contains placeholder auto
void good4(std::convertible_to<unsigned> auto...i) {}

// Error, no pack expansion or placeholder
template<unsigned = 0> void bad(unsigned...i) {}

This seems to make it impossible to declare a function that takes a variable number of parameters of a specific type. Of course, good2 above will do it, but you have to specify some number of dummy template arguments, as in good2<0,0,0>(1,2,3). good3 sort of does it, except if you call good3(1,2,3) it will fail and you have to write good3(1U,2U,3U). I'd like a function that works when you say good(1, 2U, '\003')--basically as if you had an infinite number of overloaded functions good(), good(unsigned), good(unsigned, unsigned), etc.

good4 will work, except now the arguments aren't actually of type unsigned, which could be a problem depending on context. Specifically, it could lead to extra std::string copies in a function like this:

void do_strings(std::convertible_to<std::string_view> auto...s) {}

My questions are:

  1. Am I missing some trick that would allow one to write a function that takes a variable number of arguments of a specific type? (I guess the one exception is C strings, because you can make the length a parameter pack as in template<std::size_t...N> void do_cstrings(const char(&...s)[N]) {/*...*/}, but I want to do this for a type like std::size_t)

  2. Why does the standard impose this restriction?

update

康桓瑋 asked why not use good4 in conjunction with forwarding references to avoid extra copies. I agree that good4 is the closest to what I want to do, but there are some annoyances with the fact that the parameters are different types, and some places where references do not work, either. For example, say you write code like this:

void
good4(std::convertible_to<unsigned> auto&&...i)
{
  for (auto n : {i...})
    std::cout << n << " ";
  std::cout << std::endl;
}

You test it with good(1, 2, 3) and it seems to work. Then later someone uses your code and writes good(1, 2, sizeof(X)) and it fails with a confusing compiler error message. Of course, the answer was to write for (auto n : {unsigned(i)...}), which in this case is fine, but there might be other cases where you use the pack multiple times and the conversion operator is non-trivial and you only want to invoke it once.

Another annoying problem arises if your type has a constexpr conversion function that doesn't touch this, because in that case the function won't work on a forwarding reference. Admittedly this is highly contrived, but imagine the following program that prints "11":

template<std::size_t N> std::integral_constant<std::size_t, N> cnst = {};

constexpr std::tuple tpl ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9');

inline const char *
stringify(std::convertible_to<decltype(cnst<1>)> auto...i)
{
  static constexpr const char str[] = { get<i>(tpl)..., '\0' };
  return str;
}

int
main()
{
  std::cout << stringify(cnst<1>, cnst<1>) << std::endl;
}

If you change the argument to stringify to a forwarding reference stringify(std::convertible_to<decltype(cnst<1>)> auto&&...i), it will fail to compile because of this.

update2

Here's a more comprehensive example showing why good4 isn't quite good enough if you want to avoid extra moves/copies:

#include <concepts>
#include <iostream>
#include <initializer_list>
#include <concepts>

struct Tracer {
  Tracer() { std::cout << "default constructed" << std::endl; }
  Tracer(int) { std::cout << "int constructed" << std::endl; }
  Tracer(const Tracer &) { std::cout << "copy constructed" << std::endl; }
  Tracer(Tracer &&) { std::cout << "move constructed" << std::endl; }
  void do_something() const {}
};

void
f1(Tracer t1, Tracer t2, Tracer t3)
{
  t1.do_something();
  t2.do_something();
  t3.do_something();
}

void
f2(std::convertible_to<Tracer> auto ...ts)
{
  (Tracer{ts}.do_something(), ...); // binary fold over comma
}

void
f3(std::convertible_to<Tracer> auto&& ...ts)
{
  (Tracer{std::forward<decltype(ts)>(ts)}.do_something(), ...);
}

void
f4(std::initializer_list<Tracer> tl)
{
  for (const auto &t : tl)
    t.do_something();
}

void
f5(std::convertible_to<Tracer> auto&& ...ts)
{
  std::initializer_list<Tracer> tl { std::forward<decltype(ts)>(ts)... };
  for (const auto &t : tl)
    t.do_something();
}

int
main()
{
  Tracer t;
  std::cout << "=== f1(t, 0, {}) ===" << std::endl;
  f1(t, 0, {});
  std::cout << "=== f2(t, 0, Tracer{}) ===" << std::endl;
  f2(t, 0, Tracer{});
  std::cout << "=== f3(t, 0, Tracer{}) ===" << std::endl;
  f3(t, 0, Tracer{});
  std::cout << "=== f4({t, 0, {}}) ===" << std::endl;
  f4({t, 0, {}});
  std::cout << "=== f5(t, 0, Tracer{}) ===" << std::endl;
  f5(t, 0, Tracer{});
  std::cout << "=== done ===" << std::endl;
}

The output of the program is:

default constructed
=== f1(t, 0, {}) ===
default constructed
int constructed
copy constructed
=== f2(t, 0, Tracer{}) ===
default constructed
copy constructed
copy constructed
int constructed
copy constructed
=== f3(t, 0, Tracer{}) ===
default constructed
copy constructed
int constructed
move constructed
=== f4({t, 0, {}}) ===
copy constructed
int constructed
default constructed
=== f5(t, 0, Tracer{}) ===
default constructed
copy constructed
int constructed
move constructed
=== done ===

We are trying to replicate an inifinite sequence of overloaded functions that behave like f1, which is what the rejected P1219R2 would have given us. Unfortunately, the only approach that doesn't require an extra copy is to take a std::initializer_list<Tracer>, which requires an extra set of braces on function invocation.

Author:user3188445,eproduced under the CC 4.0 BY-SA copyright license with a link to the original source and this disclaimer.
Link to original article:https://stackoverflow.com/questions/72420722/why-must-c-function-parameter-packs-be-placeholders-or-pack-expansions
dfrib :

\nWhy does the standard impose this restriction?\n\nI'll focus on the "why's", as other answer already visits various workarounds.\nP1219R2 (Homogeneous variadic function parameters) went as far as EWG\n\n# EWG incubator: in favor\nSF F N A SA\n5 2 3 0 0\n\n\nBut was eventually rejected for C++23 by EWG\n\nSF F N A SA\n2 8 8 9 2\n\n\nI think the rationale was that whilst the proposal was very well-written the actual language facility was not an essentially useful one, and particularly not enough to hold its weight given that it's a breaking change due to the C varargs comma mess:\n\nThe varargs ellipsis was originally introduced in C++ along with function prototypes. At that time, the feature did not permit a comma prior to the ellipsis. When C later adopted these features, the syntax was altered to require the intervening comma, emphasizing the distinction between the last formal parameter and the varargs parameters. To retain compatibility with C, the C++ syntax was modified to permit the user to add the intervening comma. Users therefore can choose to provide the comma or leave it out.\nWhen paired with function parameter packs, this creates a syntactic ambiguity that is currently resolved via a disambiguation rule: When an ellipsis that appears in a function parameter list might be part of an abstract (nameless) declarator, it is treated as a pack declaration if the parameter's type names an unexpanded parameter pack or contains auto; otherwise, it is a varargs ellipsis. At present, this rule effectively disambiguates in favor of a parameter pack whenever doing so produces a well-formed result.\nExample (status quo):\ntemplate <class... T>\nvoid f(T...); // declares a variadic function template with a function parameter pack\n\ntemplate <class T>\nvoid f(T...); // same as void f(T, ...)\n\nWith homogeneous function parameter packs, this disambiguation rule\nneeds to be revisited. It would be very natural to interpret the\nsecond declaration above as a function template with a homogeneous\nfunction parameter pack, and that is the resolution proposed here. By\nrequiring a comma between a parameter list and a varargs ellipsis, the\ndisambiguation rule can be dropped entirely, simplifying the language\nwithout losing any functionality or degrading compatibility with C.\nThis is a breaking change, but likely not a very impactful one. [...]\n",
2022-06-01T14:58:35
Jason Liam :

\nWhy does the standard impose this restriction?\n\nMost likely because this would confuse(or create complications) users with the old C varargs function that have almost same syntax(with unnamed parameter) as shown below:\n//this is a C varargs function\nvoid good3(int...) \n ^^^^^^\n{\n \n}\n\nNow if the fourth function bad was allowed:\n \ntemplate<int = 0> void bad(int...i)\n ^^^^^^^ --->this is very likely to confuse users as some may consider it as a C varargs function instead of a function parameter pack \n{\n}\n\nIMO the above seems atleast a little ambiguous due to the similarity in the syntax of C varargs and a function parameter pack.\nFrom dcl.fct#22:\n\nThere is a syntactic ambiguity when an ellipsis occurs at the end of a parameter-declaration-clause without a preceding comma.\nIn this case, the ellipsis is parsed as part of the abstract-declarator if the type of the parameter either names a template parameter pack that has not been expanded or contains auto; otherwise, it is parsed as part of the parameter-declaration-clause.\n\n\n\nI'd like a function that works when you say good(1, 2U, '\\003')\n\nYou can use std::common_type for this.\ntemplate <typename... T, typename l= std::common_type_t<T...>>\nvoid func(T&&... args) {\n}\n\n\n\nAm I missing some trick that would allow one to write a function that takes a variable number of arguments of a specific type?\n\nThe good3 given in your example is very readable and should be used from C++20. Though if one is using C++17 then one way to do this is using the SFINAE principle with a combination of std::conjunction and std::is_same as shown below.\nMethod 1\nHere we simply check if all the arguments passed are of the same type.\ntemplate <typename T, typename ... Args>\nstd::enable_if_t<std::conjunction_v<std::is_same<T, Args>...>, bool>\nfunc(const T & first, const Args & ... args)\n{\n std::cout<<"func called"<<std::endl;\n return true;\n}\nint main()\n{\n func(4,5,8); //works\n //func(4,7.7); //wont work as types are different\n std::string s1 = "a", s2 = "b"; \n func(s1, s2); //works\n //func(s1, 2); //won't work as types are different\n \n}\n\nWorking demo\n\nLooking at your comment it seems you want to add one more restriction that the program should work only when\na) all the arguments are of the same type\nb) all of those matches a specific type say int, or std::string.\nThis can be done by adding a static_assert inside the function template:\nstatic_assert(std::is_same_v<T, int>, "Even though all parameters are of the same type, they are not int");\n\nMethod 2\nHere use the static_assert shown above to check if the arguments passes are of a specific type like int.\ntemplate <typename T, typename ... Args>\nstd::enable_if_t<std::conjunction_v<std::is_same<T, Args>...>, bool>\nfunc(const T & first, const Args & ... args)\n{\n static_assert(std::is_same_v<T, int>, "Even though all parameters are of the same type, they are not int");\n std::cout<<"func called"<<std::endl;\n return true;\n}\n\nint main()\n{\n func(3,3); //works\n //func(4,7.7); //wont work as types are different\n std::string s1 = "a", s2 = "b"; \n //func(s1, s2); //won't work as even though they are of the same type but not int type\n \n}\n\nWorking demo\n",
2022-05-29T03:42:56
yy